/*
 *  Copyright 2010 Farent-Solutions Ltd.
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 * 
 *       http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *  under the License.
 */

package com.farent.solutions.plugins.aribaweb.awl.completion;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.AbstractDocument;
import javax.swing.text.BadLocationException;
import javax.xml.namespace.QName;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenId;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.editor.BaseDocument;
import org.netbeans.editor.TokenItem;
import org.netbeans.modules.xml.schema.completion.spi.CompletionContext;
import org.netbeans.modules.xml.text.api.XMLDefaultTokenContext;
import org.netbeans.modules.xml.text.syntax.SyntaxElement;
import org.netbeans.modules.xml.text.syntax.XMLSyntaxSupport;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;
import org.netbeans.modules.xml.text.syntax.dom.EmptyTag;
import org.netbeans.modules.xml.text.syntax.dom.EndTag;
import org.netbeans.modules.xml.text.syntax.dom.StartTag;
import org.netbeans.modules.xml.text.syntax.dom.Tag;
import org.netbeans.api.xml.lexer.XMLTokenId;
import org.netbeans.modules.xml.axi.AbstractAttribute;
import org.netbeans.modules.xml.axi.Element;
import org.netbeans.modules.xml.schema.completion.spi.CompletionModelProvider.CompletionModel;
import org.netbeans.modules.xml.schema.model.Schema;
import org.netbeans.modules.xml.schema.model.SchemaModel;

/**
 *
 * @author user
 */
public class AwlCompletionContext extends CompletionContext
{

    public static final String A_NS = "http://www.aribaweb.org/a"; //NOI18N
    public static final String W_NS = "http://www.aribaweb.org/w"; //NOI18N
    public static final String T_NS = "http://www.aribaweb.org/t"; //NOI18N
    public static final String DEFAULT_NS = "http://www.aribaweb.org/d"; //NOI18N

    public static final String A_PREFIX = "a"; //NOI18N
    public static final String W_PREFIX = "w"; //NOI18N
    public static final String T_PREFIX = "t"; //NOI18N
    
    private static final Logger log = Logger.getLogger(AwlCompletionContext.class.getName());

    public static String[] supportedPrefixes = new String[] { A_PREFIX, W_PREFIX, T_PREFIX };
    private BaseDocument document;
    private CompletionType completionType = CompletionType.COMPLETION_TYPE_UNKNOWN;
    private SyntaxElement element;
    private List<QName> pathFromRoot;
    private TokenItem token;
    private String typedChars;
    private int completionAtOffset;
    private char lastTypedChar;
    private String attribute;
    private FileObject primaryFile;
    private HashMap<String, String> declaredNamespaces =
            new HashMap<String, String>();
    private HashMap<String, CompletionModel> modelMap =
            new HashMap<String, CompletionModel>();


    AwlCompletionContext(FileObject primaryFile, XMLSyntaxSupport support, int offset) {
        try {
            this.completionAtOffset = offset;
            this.primaryFile = primaryFile;
            this.document = support.getDocument();
            this.element = support.getElementChain(offset);
            this.token = support.getPreviousToken(offset);
            this.lastTypedChar = support.lastTypedChar();
            populateNamespaces();
        } catch (BadLocationException ex) {
            Exceptions.printStackTrace(ex);
        }
    }

    @Override
    public CompletionType getCompletionType() {
        return completionType;
    }
    
    @Override
    public String getTypedChars() {
        return typedChars;
    }

    public String getAttribute() {
        return attribute;
    }

    private void populateNamespaces() {
        declaredNamespaces.put(A_PREFIX, A_NS);
        declaredNamespaces.put(W_PREFIX, W_NS);
        declaredNamespaces.put(T_PREFIX, T_NS);
    }
    
    /**
     * At a given context, that is, at the current cursor location
     * in the document, finds the type of query that needs to be
     * carried out and finds the path from root.
     */
    public boolean initContext() {
        TokenSequence tokenSequence = getTokenSequence();
        TokenItem previousToken;
        try {
            if (isTagAttributeRequired(tokenSequence)) {
                completionType = CompletionType.COMPLETION_TYPE_ATTRIBUTE;
                pathFromRoot = getPathFromRoot(element);
                return true;
            }

            int id = 0;
            if ( token == null ) {
                completionType = CompletionType.COMPLETION_TYPE_ELEMENT;
                return true;
            }
            id = token.getTokenID().getNumericID();
            switch (id) {
                //user enters < character
                case XMLDefaultTokenContext.TEXT_ID:
                    String chars = token.getImage().trim();
                    previousToken = token.getPrevious();
                    String previousTokenText = "";
                    if ( previousToken != null ) {
                        previousTokenText = previousToken.getImage().trim();
                    }
                    //First line
                    if( ( chars == null || chars.isEmpty() ) &&
                            ( previousTokenText == null || previousTokenText.isEmpty() ) ) {
                        completionType = CompletionType.COMPLETION_TYPE_ELEMENT;
                        break;
                    }
                    if(chars != null && chars.startsWith("&")) {
                        completionType = CompletionType.COMPLETION_TYPE_UNKNOWN;
                        break;
                    }
                    if (chars != null && chars.equals("") && //previousTokenText.equals("/>")) {
                        previousTokenText.endsWith(">")) {
                        //completionType = CompletionType.COMPLETION_TYPE_UNKNOWN;
                        completionType = CompletionType.COMPLETION_TYPE_ELEMENT;
//                        pathFromRoot = getPathFromRoot(element);
                        break;
                    }
                    if(chars != null && chars.startsWith("<")) {
                        typedChars = chars.substring(1);
                        completionType = CompletionType.COMPLETION_TYPE_ELEMENT;
//                        pathFromRoot = getPathFromRoot(element);
                        break;
                    }
                    if (chars != null && previousTokenText.equals(">")) {
                        if(!chars.equals("") && !chars.equals(">"))
                            typedChars = chars;
//                        pathFromRoot = getPathFromRoot(element);
                        completionType = CompletionType.COMPLETION_TYPE_ELEMENT_VALUE;
                        break;
                    }
                    if (chars != null && !chars.equals("<") && previousTokenText.equals(">")) {
                        completionType = CompletionType.COMPLETION_TYPE_UNKNOWN;
                        break;
                    }
                    break;

                case XMLDefaultTokenContext.BLOCK_COMMENT_ID:
                    completionType = CompletionType.COMPLETION_TYPE_ELEMENT;
                    pathFromRoot = getPathFromRoot(element);
                    break;

                //start tag of an element
                case XMLDefaultTokenContext.TAG_ID:
                    if(element instanceof EndTag) {
                        completionType = CompletionType.COMPLETION_TYPE_UNKNOWN;
                        break;
                    }
                    if (element instanceof EmptyTag) {
                        EmptyTag tag = (EmptyTag) element;
                        if (CompletionUtil.isCaretInsideTag(completionAtOffset, tokenSequence)) {
                            break;
                        }
                        if ((element.getElementOffset() + 1 == completionAtOffset) ||
                            (token.getOffset() + token.getImage().length() == completionAtOffset)) {
                            completionType = CompletionType.COMPLETION_TYPE_ELEMENT;
                            pathFromRoot = getPathFromRoot(element.getPrevious());
                            break;
                        }
                        if (completionAtOffset > element.getElementOffset() + 1 &&
                            completionAtOffset <= (element.getElementOffset() + 1 +
                                                  tag.getTagName().length())) {
                            completionType = CompletionType.COMPLETION_TYPE_ELEMENT;
                            int index = completionAtOffset - element.getElementOffset() - 1;
                            typedChars = index < 0 ? tag.getTagName() :
                                tag.getTagName().substring(0, index);
                            pathFromRoot = getPathFromRoot(element.getPrevious());
                            break;
                        }
                        break;
                    }

                    if(element instanceof StartTag) {
                        if(token != null &&
                           token.getImage().trim().equals(">")) {
                            pathFromRoot = getPathFromRoot(element);
                            completionType = CompletionType.COMPLETION_TYPE_ELEMENT_VALUE;
                            break;
                        }
                        if(element.getElementOffset() + 1 == this.completionAtOffset) {
                            typedChars = null;
                        } else {
                            StartTag tag = (StartTag)element;
                            int index = completionAtOffset-element.getElementOffset()-1;
                            typedChars = index<0?tag.getTagName() :
                                tag.getTagName().substring(0, index);
                        }
                    }
                    completionType = CompletionType.COMPLETION_TYPE_ELEMENT;
                    
                    break;

                //user enters an attribute name
                case XMLDefaultTokenContext.ARGUMENT_ID:
                    break;

                //some random character
                case XMLDefaultTokenContext.CHARACTER_ID:
                    completionType = CompletionType.COMPLETION_TYPE_UNKNOWN;
                    break;

                //user enters = character, we should ignore all other operators
                case XMLDefaultTokenContext.OPERATOR_ID:
                    completionType = CompletionType.COMPLETION_TYPE_UNKNOWN;
                    break;

                //user enters either ' or "
                case XMLDefaultTokenContext.VALUE_ID: {
                    //user enters start quote and no end quote exists
                    if(token.getNext() == null) {
                        if(lastTypedChar == '\'' || lastTypedChar == '\"')
                            typedChars = null;
                        else
                            typedChars = token.getImage().substring(1,
                                token.getImage().indexOf(">"));
                    }

                    //user is inside start/end quotes
                    if(lastTypedChar != '\'' && lastTypedChar != '\"') {
                        String str = token.getImage();
                        if( str != null && !str.equals("\"\"") && !str.equals("\'\'") &&
                            (str.startsWith("\"") || str.startsWith("\'")) &&
                            (str.endsWith("\"") || str.endsWith("\'")) ) {
                            typedChars = str.substring(1, str.length()-1);
                            if(completionAtOffset == token.getOffset()+1)
                                typedChars = "";
                        }
                    }
                    attribute = element.getPrevious().toString();
                    completionType = CompletionType.COMPLETION_TYPE_ATTRIBUTE_VALUE;
                    pathFromRoot = getPathFromRoot(element);
                    TokenItem t = token;
                    while(t != null) {
                        int nId = t.getTokenID().getNumericID();
                        if(nId == XMLDefaultTokenContext.ARGUMENT_ID) {
                            attribute = t.getImage();
                            break;
                        }
                        t = t.getPrevious();
                    }
                    break;
                }

                //user enters white-space character
                case XMLDefaultTokenContext.WS_ID:
                    completionType = CompletionType.COMPLETION_TYPE_UNKNOWN;
                    TokenItem prev = token.getPrevious();
                    while( prev != null &&
                           (prev.getTokenID().getNumericID() == XMLDefaultTokenContext.WS_ID) ) {
                            prev = prev.getPrevious();
                    }
                    if( (prev.getTokenID().getNumericID() == XMLDefaultTokenContext.VALUE_ID) ||
                        (prev.getTokenID().getNumericID() == XMLDefaultTokenContext.TAG_ID) ) {
                        //no attr completion for end tags
                        if (prev.getImage().startsWith("</")) break;
                    }
                    break;

                default:
                    completionType = CompletionType.COMPLETION_TYPE_UNKNOWN;
                    break;
            }
        } catch (Exception e) {
            log.log(Level.INFO,
                e.getMessage() == null ? e.getClass().getName() : e.getMessage(), e);
            return false;
        }
        return true;
    }


    boolean initModels() {
        AWLCompletionModelProvider provider = new AWLCompletionModelProvider();
        List<CompletionModel> models = provider.getModels(this);
        for (CompletionModel completionModel : models) {
            populateModelMap(completionModel);
        }
        return true;
    }

    @Override
    public String getDefaultNamespace() {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public List<QName> getPathFromRoot() {
        return pathFromRoot;
    }

    @Override
    public FileObject getPrimaryFile() {
        return primaryFile;
    }

    @Override
    public BaseDocument getBaseDocument() {
        return document;
    }

    @Override
    public HashMap<String, String> getDeclaredNamespaces() {
        return declaredNamespaces;
    }

    private TokenSequence getTokenSequence() {
        TokenSequence tokenSequence = null;
        try {
            ((AbstractDocument) document).readLock();
            TokenHierarchy tokenHierarchy = TokenHierarchy.get(document);
            tokenSequence = tokenHierarchy.tokenSequence();
        } catch(Exception e) {
            log.log(Level.WARNING,
                e.getMessage() == null ? e.getClass().getName() : e.getMessage(), e);
        } finally {
            ((AbstractDocument) document).readUnlock();
        }
        return tokenSequence;
    }

    private boolean isTagAttributeRequired(TokenSequence tokenSequence) {
        int caretPos = completionAtOffset;

        tokenSequence.move(caretPos);
        tokenSequence.moveNext();

        Token tok = tokenSequence.token();
        if (tok == null) return false;

        TokenId tokID = tok.id();
        if (tokID.equals(XMLTokenId.TAG) && CompletionUtil.isEndTagSuffix(tok) &&
           (tokenSequence.offset() + 1 == caretPos)) { // <... /|>, | - a caret position
            return false;
        }
        boolean
            isAttributeOrSpace = tokID.equals(XMLTokenId.ARGUMENT) ||
                                 tokID.equals(XMLTokenId.WS),
            isTagLastCharFound = tokID.equals(XMLTokenId.TAG) &&
                                 (CompletionUtil.isTagLastChar(tok) ||
                                  CompletionUtil.isEndTagSuffix(tok));
        while (true) {
            if (tokID.equals(XMLTokenId.TAG)) {
                if (CompletionUtil.isEndTagPrefix(tok)) break;
                else {
                    String tagName = CompletionUtil.getTokenTagName(tok);
                    if (tagName != null) {
                        int tokOffset = tokenSequence.offset(),
                            tagNameEndPos = tokOffset + CompletionUtil.TAG_FIRST_CHAR.length() +
                                            tagName.length();
                        if ((tagNameEndPos < caretPos) &&
                            (isAttributeOrSpace || isTagLastCharFound)) {
                            return true;
                        }
                    }
                }
            }
            if (! tokenSequence.movePrevious()) break;

            tok = tokenSequence.token();
            tokID = tok.id();
            if (CompletionUtil.isEndTagSuffix(tok) || CompletionUtil.isTagLastChar(tok)) break;
        }
        return false;
    }

    /**
     * This is a 2-pass algorithm. In the 1st pass, it pushes all the relevant
     * start tags all the way to the root into the stack, root being at the top.
     * In the 2nd pass, it gets items from the stack in reverse order and checks
     * if any intermediate item can be treated as root. This is possible when dealing
     * with items from multiple namespaces.
     */
    private List<QName> getPathFromRoot(SyntaxElement se) {
        //1st pass
        Stack<Tag> stack = new Stack<Tag>();
        if(se == null)
        {
            stack.push( (Tag)se );
            return createPath( stack );
        }
        if(se instanceof EmptyTag)
            stack.push((Tag)se);
        while( se != null) {
            if( (se instanceof EndTag) ||
                (se instanceof StartTag && stack.isEmpty()) ) {
                stack.push((Tag)se);
                se = se.getPrevious();
                continue;
            }
            if(se instanceof StartTag) {
                StartTag start = (StartTag)se;
                if(stack.peek() instanceof EndTag) {
                    EndTag end = (EndTag)stack.peek();
                    if(end.getTagName().equals(start.getTagName())) {
                        stack.pop();
                    }
                } else {
                    stack.push((Tag)se);
                }
            }
            se = se.getPrevious();
        }

        //2nd pass
        return createPath(stack);
    }

    /**
     * Peeks items from the stack in reverse order and checks if that tag is a
     * root. If yes, it returns the path. If no, continues to the next tag.
     * While creating the path it always adds items to the start of the list so
     * that the returned path starts from root, all the way to the current tag.
     */
    private ArrayList<QName> createPath(Stack<Tag> stack) {
        ArrayList<QName> path = new ArrayList<QName>();
        ListIterator<Tag> tags = stack.listIterator();
        Tag tag = tags.next();
        path.add(0, createQName(tag));
        CompletionUtil.printPath(path);
        return path;
    }

    private QName createQName(Tag tag) {
        QName qname = null;
        String tagName = tag.getTagName();
        String prefix = CompletionUtil.getPrefixFromTag(tagName);
        String lName = CompletionUtil.getLocalNameFromTag(tagName);
        qname = new QName(declaredNamespaces.get(prefix), lName, prefix); //NOI18N
        return qname;
    }

    static boolean isPrefixSupported ( String prefix )
    {
        return Arrays.asList(
                AwlCompletionContext.A_PREFIX,
                AwlCompletionContext.W_PREFIX,
                AwlCompletionContext.T_PREFIX).contains( prefix );
    }

    Map<String, CompletionModel> getCompletionModelMap() {
        return modelMap;
    }

    private void populateModelMap(CompletionModel m) {
        if (m != null) {
            SchemaModel sm = m.getSchemaModel();
            if (sm != null) {
                Schema schema = sm.getSchema();
                if (schema != null) {
                    String tns = m.getTargetNamespace();
                    if (tns != null) {
                        //models with namespaces
                        modelMap.put(tns, m);
                        return;
                    }
                }
            }
        }
    }

    /**
     * Returns the combined list of CompletionModels.
     */
    public List<CompletionModel> getCompletionModels() {
        List<CompletionModel> models = new ArrayList<CompletionModel>();
        models.addAll(modelMap.values());
        return models;
    }


    public boolean canReplace(String text) {
        if(completionType == CompletionType.COMPLETION_TYPE_ELEMENT && element instanceof Tag) {
            String name = ((Tag)element).getTagName();
            if(name != null && name.equals(typedChars) && text.equals(name))
                return false;
        }
        if(completionType == CompletionType.COMPLETION_TYPE_ATTRIBUTE) {
            Element e = CompletionUtil.findAXIElementAtContext(this);
            for(AbstractAttribute a : e.getAttributes()) {
                if(a.getName().equals(typedChars))
                    return false;
            }
        }

        return true;
    }

}
